經過幾天上手 Pulumi 後,我們來看看 Pulumi 的 Input 與 Output 這兩種類型的資料。
撰寫 IaC 的時候,總是離不開對資源的管理。而幾乎每個資源都會需要提供一些參數才能建立,例如建立 VPC 的時候需要給定一個 cidrBlock 或是 IPAM Pool ID。VPC 建立好後,也會產生許多的屬性供我們取用,例如 arn、defaultNetworkAclId、defaultRouteTableId 等資料。
我們要怎麼知道一個資源需要什麼參數才能建立?它提供了什麼樣的屬性跟我們使用呢?
答案是:查詢資源的文件!
例如 AWS Route53 Record 的文件中,Record Resource Properties
區域就會詳細的告訴我們 Input 與 Output 分別有什麼。
而在 Input 中,如果有標註紅色的 *
符號,就代表這個 Input 參數是必填。例如 AWS Route53 Record
中,必填的參數為 name
、type
、zoneId
,除此之外都是選填。
往下拉也能看到 AWS Route53 Record
提供給我們什麼樣的屬性,這邊可以看到只有提供兩個,分別是 fqdn
與 id
。
在管理資源,我們很常會將某個資源的 Output 做為另一個資源的 Input 參數。
例如剛剛看到的 AWS Route53 Record
就需要 zoneId
,那 zoneId
要去哪裡找呢?
我們可以透過手動建立一個 Route53 DNS Zone,並取得該 zone 的 zoneId,再將 zoneId 填寫到 AWS Route53 Record
中。或是透過 AWS Route53 Zone
建立 zone,並將 zone 的 output 中的 zoneID 做為 Record
的 input。
將 output 傳入 input 的範例:
// 建立 Zone
const zone = new aws.route53.Zone("example", {
name: "example.com.",
});
const www = new aws.route53.Record("example", {
// 將 zone 的 output 屬性當作 record 的 input 屬性
zoneId: zone.zoneId,
name: "www.example.com.",
type: "TXT",
records: [
"Hello, World"
]
});
其實 Input、Output 的概念也不新奇,將 Output 的結果傳入 Input 也沒什麼了不起。用過其他 IaC 工具的朋友可能也習以為常。
那為啥在 Pulumi 中要特別拿出來講呢?
因為上面我們用的很順手的方式,其實在 Pulumit 中,是不同的物件型別。而在撰寫程式的時候,清楚知道輸入輸出的型別很重要,傳錯型別可能會導致編譯錯誤,或是運行錯誤。
首先我們來仔細觀察一下,TypeScript
中,aws.route53.Zone
參數的型別長怎麼樣:
export interface ZoneArgs {
comment?: pulumi.Input<string>;
delegationSetId?: pulumi.Input<string>;
forceDestroy?: pulumi.Input<boolean>;
name?: pulumi.Input<string>;
tags?: pulumi.Input<{
[key: string]: pulumi.Input<string>;
}>;
vpcs?: pulumi.Input<pulumi.Input<inputs.route53.ZoneVpc>[]>;
}
可以注意到 ZoneArgs
Interface 中所有的參數型別都是 pulumi.Input,只是這邊透過泛型定義 Input 內的型別。
接著我們再來看一下 aws.route53.Zone
類別提供我們存取的屬性內容:
export declare class Zone extends pulumi.CustomResource {
readonly arn: pulumi.Output<string>;
readonly comment: pulumi.Output<string>;
readonly delegationSetId: pulumi.Output<string | undefined>;
readonly forceDestroy: pulumi.Output<boolean | undefined>;
readonly name: pulumi.Output<string>;
readonly nameServers: pulumi.Output<string[]>;
readonly primaryNameServer: pulumi.Output<string>;
readonly tags: pulumi.Output<{
[key: string]: string;
} | undefined>;
readonly tagsAll: pulumi.Output<{
[key: string]: string;
}>;
readonly vpcs: pulumi.Output<outputs.route53.ZoneVpc[] | undefined>;
readonly zoneId: pulumi.Output<string>;
}
可以發現,所有供我們存取的屬性都是 pulumi.Output 型別。
再回過頭來看上一節建立 zone 與 record 的範例,我們就會發現奇怪的地方了。
在 record 中,我們將 zone.zoneId 這個 pulumi.Output<string>
型別的資料傳入接受 pulumi.Input<string>
的參數中。
另外我們又將純文字 (string
)的 name
、type
傳給接受 pulumi.Input<string>
之中。
到底這個 pulumi.Input 是什麼型別呢?傳泛型中指定的型別也行、與之相對應的 pulumi.Output 也行?這邊沒有說到,其實 Input 也可以是一個 Promise
型別。
可以從 Input 的定義看到,其實 Input 可以是沒有包裝過的型別、Promise、Output,這也解釋了為什麼我們可以傳遞各種不同的資料給 Input。
/**
* [Input] is a property input for a resource. It may be a promptly available T, a promise for one,
* or the output from a existing Resource.
*/
// Note: we accept an OutputInstance (and not an Output) here to be *more* flexible in terms of
// what an Input is. OutputInstance has *less* members than Output (because it doesn't lift anything).
export type Input<T> = T | Promise<T> | OutputInstance<T>;
這邊修改一下範例,展示一下如何傳送 Promise 給 Input:
const www = new aws.route53.Record("example", {
zoneId: zone.zoneId,
// 傳遞 Promise<string> 至 Input<string> 中
name: Promise.resolve("www.example.com."),
type: "TXT",
records: [
"Hello, World"
]
});
看過了 Input 可以接受不同的型別後,我們來看 Output。
Output 就與 Input 不同了,Output 是真正將值封裝成一個物件,並且提供一些方式來存取他。我們可以從定義看得出來他的不平凡:
export type Output<T> = OutputInstance<T> & Lifted<T>;
但很不幸的,Pulumi 的 Output 並沒有提供任何方法、屬性讓我們直接存取到裡面所封裝的值。例如說我們拿到了一個 Output<string>
型別的值,我們是沒有任何方式可以取得裡面的資料的!
這個設計就造就了在撰寫 Pulumi 程式時的各種問題,因為無法取得 Output 內的值,對於撰寫 iac 程式很不方便。怎麼說呢?請看以下例子:
// 我們從某個 Resource 拿到了 ip address,假設是 200.100.10.20 好了
const myIp: pulumi.Output<string> = eip.publicIp
// 我們想要將 ip 字串與其他字串做一些處理,例如取得該 ip 的網段,並產生 CIDR 格式的網段描述,類似這樣: 200.100.10.0/24
// 產生出來後,我想要將他做為其他資源的 Input
以上的範例中,我們想要對 myIp 內封裝的 string 做處理,並傳送至其他資源中使用,但 Output 中沒有提供任何方式可以拿到 string
,這要怎麼辦?
當然在 Pulumi 一定有方法可以處理這個問題,不然這個 IaC 工具就會很難被使用。那要怎麼處理呢?
Pulumi 的 Output 提供了一些函式讓我們可以將 Output 的內容做一些轉換,轉換成另一個 Output。
這個概念很重要:Pulumi 的 Output 提供方式讓我們將 Output 轉換為另一種 Output
我們永遠無法拿到解開的內容,但可以透過轉換的方式將 Output 轉成另一種樣子的 Output。反正 Input 可以接受一個 Output。
為何 Pulumi 要這樣設計?要怎麼將 Output 轉成另一種 Output 呢?我們明天再來介紹 Output 的詳細用法。
今天一直猶豫要繼續重構之前的內容,然後把 stack output 講完。還是該面對 Input 與 Output。還是寫了 Input Output 的內容了,這個概念在 Pulumi 中太重要了,沒有學會的話,很多操作會遇到問題。stack output 就留到 Input Output 之後,在繼續講下去吧。